Глава 13

ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ (DLL)

13.1. НАЗНАЧЕНИЕ

Динамически подключаемые библиотеки (далее DLL - Dynamic Link Library) представляют собой универсальный механизм интегрирования в вашу программу процедур и функций, написанных другими программистами и, в общем случае, на других, чем Object Pascal, языках программирования.

DLL реализуются в виде исполняемых модулей, содержащих готовые к работе процедуры, функции и/или ресурсы. С точки зрения программиста, есть много общего между DLL и обычными для Object Pascal модулями, т. к. в конечном счете и библиотеки, и модули поставляют подпрограммы, избавляющие программиста от написания собственного кода. Но есть и принципиальные отличия. Главным из них является то, что DLL не в состоянии поставлять в программу переменные, константы и типы, ведь создатели DLL могут использовать не типизированные языки программирования, например, язык ассемблера. В результате DLL не могут экспортировать в программу столь необходимые сегодня программисту классы - для этого используются пакеты.

Другим важным отличием является способ связывания экспортируемых подпрограмм с основной программой. Модули связываются с программой на этапе компоновки, т. е. статически. Если загружены две программы, использующие одни и те же модули, в памяти будут два экземпляра одинаковых фрагментов кода. В отличие от этого DLL подключаются к программе в момент ее исполнения, т. е. динамически. Если опять же две программы используют одну и ту же DLL, в памяти будет лишь один экземпляр разделяемого программами кода. Следует уточнить, что речь идет о физической памяти компьютера. Поскольку каждая программа получает в свое распоряжение виртуальное адресное пространство (подробнее о процессах и памяти говорится в гл. 14), в эти пространства будут отображаться столько образов DLL, сколько программ ее используют (см. в главе 11 раздел “Отображение файлов в память”).

Динамическое подключение DLL дает им еще одно немаловажное преимущество над модулями: изменение любой DLL в большинстве случаев не требует перекомпиляции использующей ее программы.

13.2. РЕАЛИЗАЦИЯ

Для создания DLL в Object Pascal введено зарезервированное слово Library, которым должен начинаться текст библиотеки. За словом Library следует правильный идентификатор, но в отличие от объявления модуля он не обязан совпадать с именем файла: имя DLL определяется именем DLL-файла, а не идентификатором, следующим за Library.

Структура текста DLL повторяет структуру обычной программы с тем исключением, что раздел исполняемых операторов в DLL играет ту же роль, что и инициирующая часть модуля: операторы этой части исполняются только один раз в момент загрузки библиотеки в память. Каждое очередное обращение с требованием загрузить библиотеку наращивает на единицу ее счетчик ссылок, но не приводит к выполнению операторов исполняемой части.

В разделе описаний DLL могут объявляться типы (в том числе и классы), константы и переменные, но они остаются скрытыми от вызывающей программы и могут использоваться только внутри DLL. В разделе описаний помимо стандартных для обычной программы объявлений используется специальный раздел объявления экспортируемых подпрограмм. Этот раздел начинается зарезервированным словом Exports, за которым через запятую перечисляются имена экспортируемых подпрограмм, например:

Library MyLibrary;

Function MyFunc (...):...;

begin

end;

Procedure MyProc;

begin

end;

Exports

MyFunc, MyProc;

begin

end.

Раздел Exports помогает компилятору и компоновщику создать специальный заголовок DLL-модуля, в котором перечисляются имена подпрограмм и адреса их точек входа. В DLL может быть несколько списков Exports, но перечисляемые в них подпрограммы должны быть описаны где-то выше по тексту библиотеки.

Помимо имени подпрограммы в заголовок DLL помещается также ее порядковый номер, точнее, присвоенный ей целочисленный индекс. Это позволяет вызывающей программе ссылаться не на имя, а на индекс подпрограммы и тем самым уменьшить затраты времени на установление с ней связи. Индекс присваивается подпрограмме по порядку ее появления в списках Exports: первая подпрограмма в первом списке получает индекс 0, следующая - 1 и т. д. Программист может изменить умалчиваемую индексацию и явно указать индекс подпрограммы, добавив за ее именем в списке Exports слово index и целое число без знака в диапазоне от 0 до 32767:

Expots

MyFunc index 1, MyProc index 2;

Программист может определить внешнее имя экспортируемой подпрограммы отличным от ее настоящего имени. Для этого в списке Exports добавляется слово name и внешнее имя в апострофах:

Exports

MyFunc index I name 'NEWFUNC';

Вызывающая программа может ссылаться или на имя экспортируемой подпрограммы, или на ее индекс. При вызове по имени программа просматривает имена в таблице имен в поисках нужного. Так как имена могут состоять из длинных наборов символов и самих имен в таблице может быть много, процесс поиска имени медленнее, чем процесс поиска индекса. Поэтому опытные программисты предпочитают ссылаться не на имя, а на индекс подпрограммы.

Замечу, что в отличие от модулей Delphi не компилирует DLL автоматически в режимах make или build, т. к. справедливо рассматривает ее как другую программу, никак не связанную в момент компиляции с основной программой.

13.3. ПРИМЕР

Рассмотрим пример создания DLL, в котором иллюстрируются различные приемы объявления экспортируемых подпрограмм. Для примера выбран модуль cmpix, описанный в гл. 12. В его состав входят 4 процедуры, реализующие действия с комплексными числами. Вариант соответствующей DLL показан ниже.

Для создания заготовки библиотечного модуля выберите опцию меню File | New | Unit или в окне репозитория щелкните по пиктограмме Da. В ответ Delphi откроет специальное окно проекта с длинным комментарием, в котором указывается на необходимость вставить ссылку на модуль ShareMem, если библиотека экспортирует длинные строки в параметрах обращения к подпрограммам или как результат функций. Эта ссылка должна быть первой как в предложении uses библиотеки, так и в uses файла проекта программы, которая использует эту библиотеку. Если подпрограммы библиотеки экспортируют строки ShortString или PChar, ссылаются на ShareMem не обязательно. Сразу же сохраните проект под именем Сmpix, чтобы Delphi автоматически исправила имя библиотеки в предложении Library.

Library Cmpix;

uses

SysUtils, Classes;

{$R *.RES}

type

TComplex = record Re, Im: Real;

end; function AddC(x, y: TComplex): TComplex; stdcall;

begin

Result.Im := x.Im + y.Im;

Result.Re := x.Re + y.Re end;

function SubC(x, y: TComplex): TComplex;

stdcall;

begin

Result.Im := x.Im - y.Im;

Result.Re := x.Re - y.Re

end;

function MulC(x, у: TComplex): TComplex;

stdcall;

begin

Result.Re := x.Re * y.Re + x.Im * y.Im;

Result.Im := x.Re * y.Im - x.Im * y.Re

end;

function DivC(x, y: TComplex): TComplex;

stdcall;

var

z: Real;

begin

z := sqr(y.Re) + sqr(y.Im);

try

Result.Re := (x.Re * y.Re + x.Im * y.Im)/z;

Result.Im := (x.Re * y.Im - x.Im * y.Re)/z

except

Result.Re := le+309;

Result.Im := le+309

end

end;

Exports

AddC index 1 name 'ADDC' resident,

SubC index 2,

MulC index 3,

DivC index 4;

begin

end.

Обратите внимание: все функции нашей DLL используют соглашение stdcall, которое обеспечивает совместимость новых функций с функциями API Windows 32. Мы могли бы не указывать это соглашение; в этом случае компилятор использовал бы более эффективное соглашение register, но обращение к нашей DLL из программ, написанных на других языках программирования, в общем случае стало бы невозможным.

Если вы создали DLL для "внешнего" исользования (внеDelphi), объявляйте подпрограммы с директивой stdcall или safecall!  

Для использования подпрограмм из DLL необходимо описать их как внешние, добавив за словом External имя библиотеки в апострофах:

Procedure MyProc; External 'MyDLL';

Как уже говорилось, подпрограмма вызывается по имени или по индексу. В нашем примере из библиотеки MyDLL вызывается подпрограмма с внешним именем 'мургос'. Если нужно сослаться на индекс подпрограммы, за именем библиотеки указывается слово index и индекс:

Procedure MyProc; External 'MyDLL' index 2;

В этом случае имя, под которым подпрограмма будет известна программе, может не совпадать с ее внешним DLL-именем. Впрочем, программист может и явно переопределить имя подпрограммы, даже если он ссылается на ее внешнее имя:

Procedure MyProc; External 'MyDLL' Name 'ExtName';

В этом варианте предполагается, что экспортируется процедура с внешним именем ' ExtName '.

После любого из указанных выше объявлений экспортируемая подпрограмма становится доступна программе и может вызываться в ней как обычная подпрограмма Object Pascal.

13.4. ИСПОЛЬЗОВАНИЕ

13.4.1. Статическая загрузка

В следующей программе используется библиотека Сmpix, описанная на предыдущей странице.

type

TComplex = record Re, Im: Real;

end;

function ADDC(x, y: TComplex): TComplex; stdcall; External 'Cmplx' ;

function SubC(x, y: TComplex): TComplex; stdcall; External 'Cmplx' ;

function MulC(x, y: TComplex): TComplex; stdcall; External 'Cmplx' ;

function DivC(x, y: TComplex): TComplex; stdcall; External 'Cmplx';

procedure TfmExample.bbRunClick(Sender: TObject);

var

x,y,z: TComplex;

..... // Далее смотри текст обработчика bbRunClick в п.12.6 end; end.

Обратите внимание: библиотечная функция cmpixAdd имеет внешнее имя addc. Именно так (заглавными буквами) описана эта функция в приведенном выше примере. Если бы мы использовали function AddC(x, у: TComplex): TComplex; stdcall; External

'Cmplx';

компоновщик не смог бы ее идентифицировать.

13.4.2. Динамическая загрузка

Описанный выше способ определения функций и процедур DLL (с помощью директивы External) заставит компилятор поместить в заголовок программы список всех DLL, и загрузчик загрузит библиотеки в память одновременно с загрузкой самой программы. Программа может загружать DLL и без External с помощью трех

Стандартных функций : LoadLibrary, GetProcAddress И FreeLibrary.

Следующий пример иллюстрирует технику такой загрузки DLL Cmplx:

type

TComplex = record Re, Im: Real;

end;

TComplexFunc = function (x, y: TComplex): TComplex;

stdcall;

procedure TfmExample.bbRunClick(Sender: TObject);

var

x,y,z: TComplex;

AddC,SubC,MulC,DivC: TComplexFunc;

Handle: LongWord;

procedure Output(Operation: Char);

....//Тело процедуры Output остается без изменений

end; //Output

begin //bbRunClick

// Загружаем библиотеку CMPLX.DLL Handle := LoadLibrary('Cmplx.dll');

if Handle = 0 then

begin

ShowMessage('He найдена библиотека CMPLX.DLL') ;

Halt end;

{Определяем адреса функций. Три первые вызываем по индексу, четвертую - по имени. При вызове по индексу младшее слово PChar должно содержать индекс, поэтому делаем приведение типов:} @AddC := GetProcAddress(Handle,PChar(Longint (1)));

PSubC := GetProcAddress(Handle,PChar(Longint(2)));

@MulC := GetProcAddress(Handle,PChar(Longint(3)));

@DivC := GetProcAddress(Handle,'DivC');

x.re := Random;

x.im := Random;

y.re := Random;

y.im := Random; Output('+');

Output('-');

Output ('*');

Output ('/');

mmOutput.Lines.Add('');

// Освобождаем библиотеку FreeLibrary(Handle)

end;

13.4.3. Интерфейсный модуль

При вызове DLL-подпрограмм в большинстве случаев бывает необходимо передавать структурированные параметры типа записей, как тип TComplex в предыдущем примере. Поскольку DLL не могут экспортировать типы, приходится объявлять эти типы в вызывающей программе. Если вы часто обращаетесь в своих программах к той или иной DLL, удобно создать интерфейсный модуль, содержащий объявления как подпрограмм, так и связанных с ними типов. Например:

Unit Complx;

Interface

type

TComplex = record Re, Im: Real;

end;

function AddC(x, y: TComplex): TComplex; stdcall;

External 'Cmplx' index 1;

function SubC(x, y: TComplex): TComplex; stdcall;

External 'Cmplx' index 2;

function MulC(x, y: TComplex): TComplex; stdcall;

External 'Cmplx' index 3;

function DivC(x, y: TComplex): TComplex; stdcall;

External 'Cmplx' index 4;

Implementation end.

Такой интерфейсный модуль существенно упрощает разработку основной программы: в нашем примере он обеспечивает такой же интерфейс к библиотеке cmpix, как описанный выше модуль cmpix к своим объектам.

При обращении к подпрограммам DLL, написанным на других языках программирования, может оказаться, что внешнее имя подпрограммы содержит символы, которые не могут содержаться в правильном идентификаторе Delphi. Например, язык C++ разрешает использовать в идентификаторах символ “@”. В этом случае (а также если вы хотите переименовать экспортируемую из DLL подпрограмму) именуйте подпрограмму любым правильным с точки зрения Delphi идентификатором и укажите истинное имя подпрограммы после слова name. Например:

function MyFunction: WordBool; stdcall;

external 'MyDLL' name '_MyFunction@12'

13.5. ВКЛЮЧЕНИЕ В БИБЛИОТЕКУ ФОРМ

Несмотря на то, что DLL не имеет собственной формы, с ее помощью можно вызывать формы из связанных с библиотекой модулей. Для этого в библиотеке используется ссылка uses на связанные модули-формы и объявляются экспортируемые из DLL подпрограммы, в которых реализуется вызов соответствующих форм.

В следующем примере иллюстрируется техника включения в DLL формы и использования ее в вызывающей программе.

Текст DLL

library DLLWithForm;

uses

SysUtils,

Classes,

DLLFormU in 'DLLFormU.pas' {DLLForm};

{$R *.RES}

exports

ShowModalForm, ShowForm, FreeForm;

begin

end.

Текст формы в DLL

 

unit DLLFormU;

interface

uses

Windows, Messages, SysUtils,Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Buttons;

type

TDLLForm = class (TForm) BitBtnl: TBitBtn;

BitBtn2: TBitBtn;

procedure FormClose(Sender: TObject; var Action: TCloseAction);

private

{ Private declarations }

CallForm: THandle; //Дескриптор вызывающей формы

public

{ Public declarations }

end;

// Объявление экспортируемых подпрограмм

function ShowModalForm: Integer;

procedure ShowForm(aHandle: THandle);

procedure FreeForm;

var

DLLForm: TDLLForm;

implementation

{$R *.DFM}

function ShowModalForm: Integer;

// Модальный вызов

begin

DllForm := TDllForm.Create(Application);

Result := DLLForm.ShowModal;

DLLForm.Free;

end;

procedure ShowForm(Appl, Form: THandle);

// Немодальный вызов

begin

Application.Handle := Appl; // Замена объекта

Application DllForm := TDllForm.Create(Application);

// Запоминаем дескриптор вызывающего окна для посылки

// ему сообщения о закрытии

CallForm := Form;

DLLForm.Show

end;

procedure FreeForm;

// Уничтожение формы

begin

DLLForm.Free

end; procedure TDLLForm.FormClose(Sender: TObject;

var Action: TCloseAction);

begin

if CallFormoO then

SendMessage(CallForm, wm_User, 0, 0)

end;

end.

Текст вызывающей программы

unit TestMainU;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;

type

TTestMain = class (TForm)

Buttoni: TButton; // Открыть в модальном режиме

Button2: TButton; // Открыть в немодальном режиме

Button3: TButton; // Закрыть окно

Label I: TLabel;

procedure ButtonlClick(Sender: TObject);

procedure Button2Click(Sender: TObject);

procedure ButtonSClick(Sender: TObject);

private

{ Private declarations } public

{ Public declarations }

procedure WMUser(var Msg: TMessage);

message WM_USER;

end;

var

TestMain: TTestMain;

implementation

{$R *.DFM}

function ShowModalForm: Integer;

External 'DLLWithForm';

procedure ShowForm(Appl, Form: THandle);

External ' DLLWithForm' ;

procedure FreeForm;

External 'DLLWithForm';

procedure TTestMain.ButtonlClick(Sender: TObject);

// Модальный вызов

begin

Button2.Enabled := False;

labell.Caption := 'ModalResult = '+IntToStr(ShowModalForm);

labell.Show; // Показываем результат вызова

Button2.Enabled := True

end;

procedure TTestMain.Button2Click(Sender: TObject);

// Немодальный вызов

begin

Buttoni.Enabled :== False;

Button2.Enabled := False;

Buttons.Enabled := True; label 1.Hide;

ShowForm(Application.Handle, Self.Handle) ;

end;

procedure TTestMain.Button3Click(Sender: TObject);

// Закрыть форму

begin

FreeForm;

Button1.Enabled := True;

Button2.Enabled := True;

Button3.Enabled := False/end;

procedure TTestMain.WMUser(var Msg: TMessage) ;

// Сообщение из формы DLL о ее закрытии

begin

Buttons.Click

end;

end.

Модуль формы DLLForm, помещенной в DLL, ссылается на стандартный модуль Forms и таким образом получает свой глобальный объект Application, который ничего “не знает” о глобальном объекте вызывающей программы (см. гл. 21). В режиме модального вызова это не имеет особого значения, т. к. модальное окно блокирует работу вызывающей программы. В режиме немодального вызова следует синхронизовать действия объектов, в противном случае минимизация главного окна, например, не приведет к минимизации окна DLL. Синхронизация достигается тем, что дескриптор объекта Application DLL заменяется на соответствующий дескриптор вызывающей программы.

При показе формы в немодальном режиме она может быть закрыта щелчком по собственной системной кнопке закрыть. В этом случае она должна каким-то образом известить вызывающую программу об этом событии. Для этого используется стандартный механизм посылки вызывающей форме Windows-сообщения. Сообщение должно иметь адрес, в роли которого используется дескриптор окна, получающего это сообщение. Вот почему вторым параметром обращения к функции ShowForm в DLL передается и в поле CallForm: запоминается дескриптор вызывающего окна. Обработчик события enclose формы проверяет это поле и, если оно определено, посылает вызывающему окну сообщение с индексом wm_user. В вызывающей программе предусмотрен обработчик этого сообщения, в котором реализуются необходимые действия.